Код и жизнь. Графики всего подряд

Импортируем файл с историей

path = "result.json"; history = Import[path, "RawJSON"]["messages"];

Изучаем структуру данных

Всего сообщений:

Length @ history Для примера посмотрим на первое и девятое сообщение, чтобы понять что в них есть интересного

history[[1]] history[[9]] Первое сообщение - сервисное, на что указывает его тип.

Там еще есть `"action"`. Кроме всеких переименований,

есть "действие" присоединения участника в группе.

В будущем оно нам понадобится.

Второе сообщение с форматированием состоит из нескольких блоков, вот как их можно объединить

Message9 = StringJoin @ Map[If[StringQ[#], #, #["text"]]&] @ If[StringQ[#], {#}, #]& @ history[[9, "text"]] Мы получили голую строку. Но чтобы посмотреть на нее примерно так,

как она рендерится в приложении, я могу использовать один хитрый трюк -

вставить перемунную в MD-разметку:

такс

тут вопрос, говорят, что можно только за звезды писать.

вроде бы нет такой настройки?

А вот так можно извлечь классы

Reactions3 = Values @ history[[3, "reactions", All, {"emoji", "count"}]] И посмотреть что это за реакции:

.wlx <Grid><Reactions3 /></Grid>

Виды графиков

Прежде чем строить конкретные графики, я приведу несколько базовых примеров.

Просто линия или точки

Show[ Plot[{Sin[x], BesselJ[0, x]}, {x, 0, 15}, ImageSize -> 600, Frame -> True, GridLines -> Automatic ], ListPlot[Table[{x, Cos[x]}, {x, 0, 15, 0.1}], PlotStyle -> Directive[Darker[Green], PointSize[0.025]] ] ]

Чарты

PieChart[RandomInteger[10, 5]] Histogram[Map[Total] @ RandomReal[1, {1000, 1000}], ColorFunction -> "TemperatureMap", ImageSize -> {600, 400}, Frame -> True, GridLines -> Automatic ]

Контуры

ContourPlot[Sin[x] * Cos[y], {x, -2, 2}, {y, -2, 2}, ImageSize -> {500, 500} ]

3D

Plot3D[Sin[x] * Cos[y], {x, -2, 2}, {y, -2, 2}, ImageSize -> {600, 600}, ColorFunction -> "TemperatureMap" ]

Графики группы!

Ну и наконец перейдем к группе

Для всех графиков я заранее создам единое хранилище.

Оно нам понадобится ниже:

ChatReport = <||>;

Общая активность в группе

Для начала возьмем даты всех событий и приведем тип к `DateObject`:

activityTimeline = MapIndexed[{#, #2[[1]]}&] @ Map[FromUnixTime] @ Map[ToExpression] @ Query[All, "date_unixtime"] @ Select[KeyExistsQ[#, "date_unixtime"]&] @ history; А теперь построим линию на шкале времени, где камулятивно количество событий будет расти:

ChatReport["Общая активность в группе"] = DateListPlot[{activityTimeline}, ImageSize -> {600, 400}, PlotStyle -> Red, Frame -> True ] Та же самая активность, но теперь не по нарастающей,

а суммирование числа событи по дням:

ChatReport["Общая активность в группе по дням"] = DateHistogram[activityTimeline[[All, 1]], "Day", ImageSize -> {600, 400}, ColorFunction -> "TemperatureMap", DateTicksFormat -> {"Month", ".", "Day"}, FrameTicks -> { DateRange[DateObject[{2025, 4, 7}], Today, Quantity[2, "Days"]], Range[0, 500, 50]}, Frame -> True ]

Приглашения

Кроме общей активности, мы можем отфильтровать конкретные сообщения.

Например добавление новых участников.

Это сервисное сообщение с действием `"join_group_by_link"`.

Выберем все такие сообщения:

invatesHistory = Query[All, {"date_unixtime" /* ToExpression /* FromUnixTime, "actor"}] @ Select[#action === "join_group_by_link"&] @ Select[#type === "service"&] @ history; Теперь я могу построить историю добавления в виде такой же зависимости числа участников от времени:

ChatReport["Как пользователи добавлялись в группу"] = DateListPlot[ MapIndexed[{#1[[1]], #2[[1]]}&] @ invatesHistory, ImageSize -> {600, 400} ]

Активность пользователей

Кто писал больше всех

messages = Select[#type === "message"&] @ history; writers = SortBy[Last] @ Tally @ messages[[All, "from"]]; ChatReport["Самые активные участники"] = BarChart[writers[[-20 ;; , 2]], ColorFunction -> "TemperatureMap", ImageSize -> {800, 600}, Frame -> True, BarOrigin -> Left, ChartLabels -> writers[[-20 ;; , 1]], PlotRange -> {{0, 1000}, {0, 21}}, GridLines -> Automatic ] Конечно я могу выбрать самомго активного пользователя и посмотреть на его личную активность

topWriter = writers[[-1, 1]]; topWritesMessages = Map[FromUnixTime[ToExpression[#["date_unixtime"]]]&] @ Select[#from === topWriter&] @ messages; ChatReport["Самые активный участник"] = DateHistogram[topWritesMessages, "Day", PlotLabel -> Style["Активность участника " <> topWriter, FontSize -> 20], ImageSize -> {800, 400}, ColorFunction -> "Rainbow", PlotRange -> {{AbsoluteTime[DateObject[{2025, 4, 6, 12, 0, 0}]], AbsoluteTime[Now]}, {0, 150}}, DateTicksFormat -> {"Month", ".", "Day"}, FrameTicks -> { DateRange[DateObject[{2025, 4, 7}], Today, Quantity[2, "Days"]], Range[0, 150, 10]}, Frame -> True ]

Связь пользователей

Кто кому отвечал. То есть связи участников друг с другом

messagesAssoc = Association @ Map[#id -> #&] @ messages; replyGraph = Map[#[[1]] -> #[[2]]&] @ Select[#[[1]] =!= #[[2]]&] @ Select[StringQ[#[[1]]] && StringQ[#[[2]]]&] @ DeleteDuplicates @ Map[Sort[{messagesAssoc[#id]["from"], messagesAssoc[#["reply_to_message_id"]]["from"]}]&] @ Select[KeyExistsQ[#, "reply_to_message_id"]&] @ messages; .sh npm i 3d-force-graph .esm import ForceGraph3D from '3d-force-graph'; core.ForceGraph3D = async (args, env) => { //load shared library from WLJS Notebook store await interpretate.shared.SpriteText.load(); //interprete input data const data = await interpretate(args[0], env); const SpriteText = interpretate.shared.SpriteText.SpriteText; const opts = await core._getRules(args, env); // Build labels mapping const labels = (opts.VertexLabels || []).reduce((acc, { lhs, rhs }) => { acc[lhs] = rhs; return acc; }, {}); // Collect node IDs and construct links const nodeIds = new Set(); const links = data.map(({ lhs, rhs }) => { nodeIds.add(lhs); nodeIds.add(rhs); return { source: String(rhs), target: String(lhs) }; }); // Create nodes with labels const nodes = Array.from(nodeIds).map(id => ({ id: String(id), label: labels[id] || String(id), })); let imageSize = (opts.ImageSize) || 350; if (!Array.isArray(imageSize)) { imageSize = [imageSize, imageSize * 0.7]; } // Initialize the 3D force graph const Graph = ForceGraph3D({})(env.element) .width(imageSize[0]) .height(imageSize[1]) .cooldownTicks(100) .graphData({ nodes, links }) .nodeThreeObject(node => { const sprite = new SpriteText(node.label); sprite.material.depthWrite = true; // Make sprite background transparent sprite.color = 'white'; sprite.textHeight = 12; return sprite; }) .nodeThreeObjectExtend(false); // Apply optional charge strength if ('Charge' in opts) { Graph.d3Force('charge').strength(opts.Charge); } Graph.onEngineStop(() => Graph.zoomToFit(400)); env.local.Graph = Graph; }; core.ForceGraph3D.destroy = () => { console.warn('3D graph was removed'); } //make each instance unique core.ForceGraph3D.virtual = true ForceGraph3D /: MakeBoxes[f_ForceGraph3D, StandardForm] := With[{ (* compress it for the case if you have many labels and vertices *) o = CreateFrontEndObject[f] }, (* low-level decoration box *) ViewBox[o, o] ]; ForceGraph3D /: MakeBoxes[f_ForceGraph3D, WLXForm] := With[{ (* compress it for the case if you have many labels and vertices *) o = CreateFrontEndObject[f] }, MakeBoxes[o, WLXForm] ]; ChatReport["Ответы друг другу"] = ForceGraph3D[replyGraph, ImageSize -> 1000 ]

ApexCharts

ApexCharts - множество видов графиков со своей стилистикой

.sh npm i apexcharts --prefix . .esm import ApexCharts from 'apexcharts' const whenVisible = (ele, cbk) => { let observer = new IntersectionObserver(function(entries) { if(entries[0].isIntersecting === true) { observer.unobserve(ele); cbk(); return; } }, { threshold: [0] }); observer.observe(ele); }; core.ApexCharts = async (args, env) => { const options = await interpretate(args[0], env); const chart = new ApexCharts(env.element, options); whenVisible(env.element, () => chart.render()) } ApexCharts /: MakeBoxes[a: ApexCharts[_Association], StandardForm ] := With[{}, ViewBox[a, a] ] ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: StandardForm ] := With[{o = CreateFrontEndObject[a]}, MakeBoxes[o, form] ] /; ByteCount[a] > 1024*4 ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: WLXForm ] := With[{o = CreateFrontEndObject[a]}, MakeBoxes[o, form] ]

Прямоугольные сектора

Другой взгляд на активность

data = Transpose @ Map[<|"x" -> #[[1]], "y" -> #[[2]]|>&] @ Tally @ messages[[All, "from"]]; ChatReport["Как у трейдеров"] = ApexCharts[<| "chart" -> <|"height" -> 500, "width"->800, "type" -> "treemap"|>, "series" -> { <| "data" -> data |> }, "plotOptions" -> <|"treemap" -> <|"distributed" -> True|>|> |>]

Паутина

Или же средняя активность всех участников в радиальном виде:

weekActivity = Tally @ Map[DateValue[#, "DayNameShort"]&] @ activityTimeline[[All, 1]] ChatReport["Радиальная активность на паутине"] = ApexCharts[<| "chart" -> <|"height" -> 500, "width"->500, "type" -> "radar"|>, "dataLabels" -> <|"enabled" -> True|>, "xaxis" -> <|"categories" -> weekActivity[[All, 1]]|>, "series" -> { <| "data" -> weekActivity[[All, 2]] |> } |>]

Реакции

А теперь более комплексный график.

По оси X - число сообщений пользователя,

по Y - средняя длина сообщений,

а размер круга - число полученных реакций.

Каждый польователь своим цветом.

textBlocksToString[text_String] := text; textBlocksToString[formatted_Association] := formatted["text"]; textBlocksToString[blocks_List] := StringJoin @ Map[textBlocksToString] @ blocks; numberOfReactions[message_Association] := If[KeyExistsQ[message, "reactions"], Total[message[["reactions", All, "count"]]], (*Else*) 0 ] reactionsMetric = SortBy[Last] @ Select[First[#] > 10&] @ Map[N[{Length[#], Mean[#[[All, "textLength"]]], Mean[#[[All, "numberOfReactions"]]]}]&] @ GroupBy[#from&] @ Map[<| "from" -> #from, "textLength" -> StringLength[textBlocksToString[#text]], "numberOfReactions" -> numberOfReactions[#]|>& ] @ Select[KeyExistsQ[#, "reactions"]&] @ messages Ну и сделаем бабл чарт

ChatReport["Реакции - связь с числом сообщений и длиной"] = ApexCharts[<| "chart" -> <|"height" -> 500, "width"->900, "type" -> "bubble"|>, "dataLabels" -> <|"enabled" -> False|>, "fill" -> <|"opacity" -> 0.5|>, "series" -> KeyValueMap[<|"name" -> #1, "data" -> {#2}|>&, reactionsMetric], "xaxis" -> <|"min" -> -30, "max" -> 400|>, "yaxis" -> <|"min" -> -100|> |>]

Plotly

Есть еще одна сторонняя библиотека для рисования - это Plotly.

Самое простое что можно - построить по точкам:

Снова реакции

А вот соотношение реакций в виде сектор-чарта:

totalReactions = Query[-10 ;; ] @ Sort @ Map[Total[#[[All, "count"]]]&] @ GroupBy[#emoji&] @ Select[KeyExistsQ[#, "emoji"]&] @ Flatten @ Map[#reactions&] @ Select[KeyExistsQ[#, "reactions"]&] @ messages ChatReport["Сектор-чарт для реакций"] = Plotly[ <| "values" -> Values[totalReactions], "labels" -> Keys[totalReactions], "type" -> "pie", "hole" -> 0.5, "title" -> <| "text" -> "Реакции", "font" -> <|"size" -> 24|> |>, "textinfo" -> "label+percent" |>, <| "height" -> 600, "width" -> 600, "paper_bgcolor" -> "#FFF0" |>]

Слайды

Я не зря каждый график сохранял в переменную.

Теперь я могу проделать вот такой трюк

.wlx ManySlides[Rule["Data", assoc_Association]] := With[{ Slides = KeyValueMap[ Function[{Key1, Value1}, <section> <h1><Key1 /></h1> <Value1/> </section> ], assoc ] }, <Slides/> ] .slide <ManySlides Data={ChatReport}/>